use alloc::string::String;
use time::{OffsetDateTime, UtcDateTime};

use crate::{
    Def, Facet, ParseError, PtrConst, PtrUninit, Shape, Type, UserType, Variance, value_vtable,
};

unsafe impl Facet<'_> for UtcDateTime {
    const SHAPE: &'static Shape = &const {
        Shape {
            id: Shape::id_of::<Self>(),
            layout: Shape::layout_of::<Self>(),
            vtable: {
                let mut vtable = value_vtable!(UtcDateTime, |f, _opts| write!(
                    f,
                    "{}",
                    Self::SHAPE.type_identifier
                ));
                {
                    vtable.try_from = {
                        Some(
                            |source: PtrConst, source_shape: &Shape, target: PtrUninit| {
                                if source_shape.is_type::<String>() {
                                    let source = unsafe { source.read::<String>() };
                                    let parsed = UtcDateTime::parse(
                                        &source,
                                        &time::format_description::well_known::Rfc3339,
                                    )
                                    .map_err(|_| ParseError::Generic("could not parse date"));
                                    match parsed {
                                        Ok(val) => Ok(unsafe { target.put(val) }),
                                        Err(_e) => Err(crate::TryFromError::Generic(
                                            "could not parse date",
                                        )),
                                    }
                                } else {
                                    Err(crate::TryFromError::UnsupportedSourceShape {
                                        src_shape: source_shape,
                                        expected: &[String::SHAPE],
                                    })
                                }
                            },
                        )
                    };
                    vtable.parse = {
                        Some(|s: &str, target: PtrUninit| {
                            let parsed = UtcDateTime::parse(
                                s,
                                &time::format_description::well_known::Rfc3339,
                            )
                            .map_err(|_| ParseError::Generic("could not parse date"))?;
                            Ok(unsafe { target.put(parsed) })
                        })
                    };
                    vtable.format.display = Some(|value, f| unsafe {
                        let udt = value.get::<UtcDateTime>();
                        match udt.format(&time::format_description::well_known::Rfc3339) {
                            Ok(s) => write!(f, "{s}"),
                            Err(_) => write!(f, "<invalid UtcDateTime>"),
                        }
                    });
                }
                vtable
            },
            ty: Type::User(UserType::Opaque),
            def: Def::Scalar,
            type_identifier: "UtcDateTime",
            type_params: &[],
            doc: &[],
            attributes: &[],
            type_tag: None,
            inner: None,
            proxy: None,
            // UtcDateTime has no lifetime parameters, so it's covariant
            variance: Variance::COVARIANT,
        }
    };
}

unsafe impl Facet<'_> for OffsetDateTime {
    const SHAPE: &'static Shape = &const {
        Shape {
            id: Shape::id_of::<Self>(),
            layout: Shape::layout_of::<Self>(),
            vtable: {
                let mut vtable = value_vtable!(OffsetDateTime, |f, _opts| write!(
                    f,
                    "{}",
                    Self::SHAPE.type_identifier
                ));
                {
                    vtable.try_from = {
                        Some(
                            |source: PtrConst, source_shape: &Shape, target: PtrUninit| {
                                if source_shape.is_type::<String>() {
                                    let source = unsafe { source.read::<String>() };
                                    let parsed = OffsetDateTime::parse(
                                        &source,
                                        &time::format_description::well_known::Rfc3339,
                                    )
                                    .map_err(|_| ParseError::Generic("could not parse date"));
                                    match parsed {
                                        Ok(val) => Ok(unsafe { target.put(val) }),
                                        Err(_e) => Err(crate::TryFromError::Generic(
                                            "could not parse date",
                                        )),
                                    }
                                } else {
                                    Err(crate::TryFromError::UnsupportedSourceShape {
                                        src_shape: source_shape,
                                        expected: &[String::SHAPE],
                                    })
                                }
                            },
                        )
                    };
                    vtable.parse = {
                        Some(|s: &str, target: PtrUninit| {
                            let parsed = OffsetDateTime::parse(
                                s,
                                &time::format_description::well_known::Rfc3339,
                            )
                            .map_err(|_| ParseError::Generic("could not parse date"))?;
                            Ok(unsafe { target.put(parsed) })
                        })
                    };
                    vtable.format.display = Some(|value, f| unsafe {
                        let odt = value.get::<OffsetDateTime>();
                        match odt.format(&time::format_description::well_known::Rfc3339) {
                            Ok(s) => write!(f, "{s}"),
                            Err(_) => write!(f, "<invalid OffsetDateTime>"),
                        }
                    });
                }
                vtable
            },
            ty: Type::User(UserType::Opaque),
            def: Def::Scalar,
            type_identifier: "OffsetDateTime",
            type_params: &[],
            doc: &[],
            attributes: &[],
            type_tag: None,
            inner: None,
            proxy: None,
            // OffsetDateTime has no lifetime parameters, so it's covariant
            variance: Variance::COVARIANT,
        }
    };
}

#[cfg(test)]
mod tests {
    use core::fmt;

    use time::OffsetDateTime;

    use crate::{Facet, PtrConst};

    #[test]
    fn parse_offset_date_time() {
        facet_testhelpers::setup();

        let target = OffsetDateTime::SHAPE.allocate().unwrap();
        unsafe {
            (OffsetDateTime::SHAPE.vtable.parse.unwrap())("2023-03-14T15:09:26Z", target).unwrap();
        }
        let odt: OffsetDateTime = unsafe { target.assume_init().read() };
        assert_eq!(
            odt,
            OffsetDateTime::parse(
                "2023-03-14T15:09:26Z",
                &time::format_description::well_known::Rfc3339
            )
            .unwrap()
        );

        {
            struct DisplayWrapper<'a>(PtrConst<'a>);

            impl fmt::Display for DisplayWrapper<'_> {
                fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
                    unsafe { (OffsetDateTime::SHAPE.vtable.format.display.unwrap())(self.0, f) }
                }
            }

            let s = format!("{}", DisplayWrapper(PtrConst::new((&odt).into())));
            assert_eq!(s, "2023-03-14T15:09:26Z");
        }

        // Deallocate the heap allocation to avoid memory leaks under Miri
        unsafe {
            OffsetDateTime::SHAPE.deallocate_uninit(target).unwrap();
        }
    }
}
